Skip to content

KTOR-9398 Client Core. Cancel streaming on exception #5437

Merged
zibet27 merged 6 commits intomainfrom
zibet27/client-cancel-streaming-on-exception
Mar 31, 2026
Merged

KTOR-9398 Client Core. Cancel streaming on exception #5437
zibet27 merged 6 commits intomainfrom
zibet27/client-cancel-streaming-on-exception

Conversation

@zibet27
Copy link
Copy Markdown
Collaborator

@zibet27 zibet27 commented Mar 11, 2026

Subsystem
Client Core

Motivation
KTOR-9398 Streaming call not cancelled when exception is thrown from HttpStatement.execute/body

Solution

  • add cause to Response.cleanup

@zibet27 zibet27 requested review from bjhham and e5l March 11, 2026 10:16
@zibet27 zibet27 self-assigned this Mar 11, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

HttpStatement execution paths now capture exceptions and pass an optional Throwable? to HttpResponse.cleanup(cause). A deprecated no-arg overload remains. Tests added to assert immediate cancellation during streaming exceptions. Multiple JS/WASM blob multipart helpers and platform-specific blob channel providers were added. Various server, auth, websockets, and utils changes also included.

Changes

Cohort / File(s) Summary
HttpStatement API & ABI
ktor-client/ktor-client-core/api/ktor-client-core.api, ktor-client/ktor-client-core/api/ktor-client-core.klib.api
HttpStatement.cleanup extended to accept a Throwable? cause; synthetic/backcompat overload preserved.
HttpStatement implementation
ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt
Execution paths capture callFailure and call response.cleanup(cause); HttpResponse.cleanup(cause: Throwable?) added plus deprecated no-arg alias; fetchResponse() updated.
Client tests
ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpStatementTest.kt
Added tests testStreamingResponseExceptionCancelsImmediately and testStreamingResponseExceptionInBodyCancelsImmediately.
Form multipart (JS/Wasm) & tests
ktor-client/ktor-client-core/web/src/io/ktor/client/request/forms/FormDsl.web.kt, ktor-client/ktor-client-core/js/.../FormDsl.js.kt, ktor-client/ktor-client-core/wasmJs/.../FormDsl.wasmJs.kt, ktor-client/ktor-client-core/js/test/.../FormDslJsTest.kt, ktor-client/ktor-client-core/wasmJs/test/.../FormDslWasmJsTest.kt
Added FormBuilder.appendBlob overloads and platform blobChannelProvider actuals; JS/Wasm implementations convert Blob→bytes and tests validating multipart parts and Content-Length added.
JS build/test infra
build-logic/.../KtorTargets.kt, karma/chrome_bin.js, ktor-client/ktor-client-core/api/ktor-client-core.klib.api
Kotlin/JS target compilerOptions set (ES2015/ES module), webpack externals stub node:net, and klib API updated for blob overloads.
Test host WebSocket session
ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/client/TestEngineWebsocketSession.kt
Removed explicit socketJob and join; run() uses suspendCancellableCoroutine with cancellation-aware invokeOnClose handling.
Java engine Host header handling
ktor-client/ktor-client-java/jvm/src/io/ktor/client/engine/java/Java.kt, .../JavaHttpRequest.kt, .../RequestProducerTest.kt
Java engine init sets jdk.httpclient.allowRestrictedHeaders to include host; Host removed from OkHttp/Java disallowed headers; tests added to verify custom Host preserved.
Jetty & HTTP/2 Host handling
ktor-client/ktor-client-jetty-jakarta/.../JettyHttpRequest.kt, .../JettyHttp2EngineTest.kt
prepareHeadersFrame visibility widened; :authority derived from optional Host header and raw Host removed to avoid duplication; tests added to assert authority from custom Host.
OkHttp engine dispatcher
ktor-client/ktor-client-okhttp/jvm/src/.../OkHttpEngine.kt, .../OkHttpEngineTest.kt
Dispatcher only configured when no preconfigured client supplied; test added ensuring preconfigured dispatcher is preserved.
Netty SSE/streaming flush
ktor-server/ktor-server-netty/.../NettyApplicationCall.kt, NettyHttpHandlerState.kt, NettyHttpResponsePipeline.kt, NettyEngineTest.kt, HttpServerCommonTestSuite.kt
Added streaming-response tracking (isStreamingResponse, streamingResponses) and adjusted flush condition; SSE/H2 flush tests added for concurrent SSE + regular request behavior.
Digest auth RFC7616
ktor-http/.../auth/*, ktor-server/ktor-server-plugins/ktor-server-auth/... (many files), .../DigestCredential.kt, several tests (DigestRFC7616Test.kt, DigestApacheClientIntegrationTest.kt, DigestTest.kt)
Introduced DigestAlgorithm, DigestQop, strong-typed DigestCredential, multi-algorithm provider, qop/userhash/charset support, API surface and compatibility wrappers, large set of tests and integration suite added.
JWT verifier suspendable
ktor-server/ktor-server-plugins/ktor-server-auth-jwt/.../JWTAuth.kt, JWTUtils.kt
Verifier support changed to suspending form; JWK retrieval moved to Dispatchers.IO (withContext).
Dynamic provider authenticate suspendable
ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/DynamicProviderConfig.kt, api/ktor-server-auth.*
authenticate now accepts suspend (AuthenticationContext) -> Unit; non-suspend overload retained as hidden wrapper; API klib updated.
WebSocket routing return type & compatibility
ktor-server/ktor-server-plugins/ktor-server-websockets/.../Routing.kt, RoutingBackwardCompatible.kt, WebSocketTest.kt, api/ktor-server-websockets.*
Route.webSocket/webSocketRaw overloads changed to return Route; deprecated hidden JVM stubs added to preserve binary compatibility; tests updated to assert returned Route.
ApplicationConfig.getAs APIs & tests
ktor-server/ktor-server-core/.../ApplicationConfig.kt, api/ktor-server-core.*, and tests MapApplicationConfigTest.kt, ConfigJvmTest.kt
Added getAs(type: TypeInfo): Any? and inline reified getAs<E>(); implemented deserialization from flattened map; tests added verifying typed config mapping from Map and YAML.
Dependency injection cleanup
ktor-server/ktor-server-plugins/ktor-server-di/.../DependencyInjection.kt, tests
Shutdown cleanup iterates dependencyMap entries in reverse and skips Ambiguous/Missing initializers; tests added to cover override/cleanup behavior.
Serialization type-parameter checking
ktor-shared/ktor-serialization/.../SerializerLookup.kt, tests
Added checkTypeParameters to surface clearer SerializationException for non-serializable generic arguments; tests added asserting message contains offending type.
Case-insensitive maps & StringValues
ktor-utils/common/src/io/ktor/util/CaseInsensitiveMap.kt, StringValues.kt, tests
Reimplemented CaseInsensitiveMap and StringValuesImpl with custom array/hash-table backing to reduce allocations; tests extended for map behavior and deduplication.
Pipeline & platform JS changes
ktor-utils/js/src/.../PipelineJs.kt (removed), web pipeline header changes, network node.net import adjustments, karma/build tweaks
Removed JS pipeline bridge, adjusted node:net imports and replaced runtime require with @JsModule("node:net") externals; JS compiler options and Karma externals adjusted to improve JS test builds.
VERSION bump & small docs/KDoc edits
VERSION, various KDoc updates
Project version bumped to 3.5.0-SNAPSHOT; several KDoc clarifications and header year updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • e5l
  • bjhham
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding cancellation of streaming responses when an exception is thrown during HttpStatement execution, directly addressing KTOR-9398.
Description check ✅ Passed The description follows the required template with Subsystem (Client Core), Motivation (KTOR-9398 issue link), and Solution (add cause to Response.cleanup). All required sections are present and substantive.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zibet27/client-cancel-streaming-on-exception

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zibet27
Copy link
Copy Markdown
Collaborator Author

zibet27 commented Mar 11, 2026

PS: Should we keep binary compatibility for the internal published API?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpClientTest.kt (1)

210-232: Test covers exception propagation well; consider adding cancellation verification.

The test correctly validates that exceptions thrown inside the execute block are properly propagated. The withTimeout(2000) ensures the test won't hang if cancellation fails.

However, the test name suggests it verifies "ImmediatelyCancels" but only validates exception propagation. To fully verify immediate cancellation, you could add an assertion that the withTimeout didn't trigger (i.e., the exception was thrown before the 2-second timeout), or verify server-side that the connection was closed promptly.

💡 Optional: Add timing assertion to verify immediate cancellation
     `@Test`
     fun testStreamingResponseExceptionImmediatelyCancels() = runBlocking {
         val client = HttpClient(factory) {
             install(HttpTimeout) {
                 requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
                 socketTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
                 connectTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
             }
         }

+        val startTime = System.currentTimeMillis()
         val exception = assertThrows<IllegalStateException> {
             withTimeout(2000) {
                 client.prepareGet("http://localhost:$serverPort/sse/delay/60000").execute { response ->
                     // Headers are received immediately
                     assertEquals(HttpStatusCode.OK, response.status)

                     // Throw an exception while waiting for the body
                     throw IllegalStateException("Test exception from execute block")
                 }
             }
         }
         assertEquals("Test exception from execute block", exception.message)
+        // Verify cancellation happened promptly, not after timeout
+        assertTrue(System.currentTimeMillis() - startTime < 1000, "Exception should propagate immediately")
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpClientTest.kt`
around lines 210 - 232, The test
testStreamingResponseExceptionImmediatelyCancels currently only checks exception
propagation; add a timing assertion to verify cancellation happened before the
withTimeout window by recording the start time before calling withTimeout
(wrapping the client.prepareGet(...).execute block) and assert the elapsed time
is well under 2000ms (e.g., < 1000ms) after the IllegalStateException is thrown;
reference the existing withTimeout,
client.prepareGet("http://localhost:$serverPort/sse/delay/60000") and execute
block to locate where to insert the start-time capture and elapsed-time
assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpClientTest.kt`:
- Around line 210-232: The test testStreamingResponseExceptionImmediatelyCancels
currently only checks exception propagation; add a timing assertion to verify
cancellation happened before the withTimeout window by recording the start time
before calling withTimeout (wrapping the client.prepareGet(...).execute block)
and assert the elapsed time is well under 2000ms (e.g., < 1000ms) after the
IllegalStateException is thrown; reference the existing withTimeout,
client.prepareGet("http://localhost:$serverPort/sse/delay/60000") and execute
block to locate where to insert the start-time capture and elapsed-time
assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6871b365-8984-42a1-b6f8-109687bd1a4a

📥 Commits

Reviewing files that changed from the base of the PR and between 7b84f09 and f59b950.

📒 Files selected for processing (4)
  • ktor-client/ktor-client-core/api/ktor-client-core.api
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
  • ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt
  • ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpClientTest.kt

@zibet27 zibet27 requested a review from osipxd March 11, 2026 12:21
Copy link
Copy Markdown
Member

@e5l e5l left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

1. body<T, R> overload not updated (Critical)

The execute(block) overload now passes callFailure to cleanup(), but the body<T, R>(block) overload at HttpStatement.kt lines 138–150 still calls response.cleanup() without a cause:

public suspend inline fun <reified T, R> body(
    crossinline block: suspend (response: T) -> R
): R = unwrapRequestTimeoutException {
    val response: HttpResponse = fetchStreamingResponse()
    try {
        ...
    } finally {
        response.cleanup() // <-- no cause passed
    }
}

This overload has the exact same hanging problem this PR fixes for execute. If a user calls statement.body<ByteReadChannel> { channel -> throw IllegalStateException() }, they'll still hang.

2. join() safety after cancel() is engine-dependent

In cleanup(cause), after cancel(CancellationException(...)) the code calls join(). Whether cancel() on the CompletableJob propagates cancellation to the underlying network channel depends on the engine implementation. If rawContent.cancel() in the if (!isSaved) guard is the only thing that actually interrupts pending I/O, and the job cancellation doesn't automatically propagate to the channel, join() could still hang for engines where the channel doesn't observe job cancellation.

Consider cancelling rawContent unconditionally (or at least before join()) on the exception path to guarantee the I/O is interrupted regardless of engine.

3. CancellationException message could be more actionable

Per project guidelines ("Make error messages actionable; include the problematic value/context"), the message "Request failed" in:

cancel(CancellationException("Request failed", cause))

could include more context (e.g., the request URL or the cause's message) to aid debugging when inspecting the job's cancellation cause from a parent coroutine.

4. Test coverage

The new test is only in the JVM engine-specific test file. Consider adding a common-level test in HttpStatementTest.kt to verify the cancellation logic across all engines. The body<T, R> overload fix (issue #1) would also need its own test.


Overall the approach is correct — cancelling vs completing the job on exception is the right fix. The main gap is the body<T, R> overload which has the identical bug but wasn't updated.

@osipxd
Copy link
Copy Markdown
Member

osipxd commented Mar 11, 2026

PS: Should we keep binary compatibility for the internal published API?

Yes, since it's marked with @PublishedApi it can be used from compiled code as a result of caller's inlining.

osipxd

This comment was marked as off-topic.

@zibet27 zibet27 force-pushed the zibet27/client-cancel-streaming-on-exception branch from 30800db to a063a62 Compare March 12, 2026 13:08
@zibet27 zibet27 changed the base branch from main to release/3.x March 12, 2026 13:09
@zibet27 zibet27 requested a review from e5l March 12, 2026 13:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/client/TestEngineWebsocketSession.kt`:
- Around line 31-41: The run() suspend function can call cont.resume/... after
the continuation is already completed, causing IllegalStateException; fix
TestEngineWebsocketSession.run by registering cont.invokeOnCancellation to set a
cancellation flag (e.g., var cancelled = false; cont.invokeOnCancellation {
cancelled = true }) and then in the outgoing.invokeOnClose callback guard the
resume/resumeWithException calls with if (!cancelled) { ... } so you only resume
when the continuation hasn't been externally completed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 297c6af5-a336-45ea-8cdd-56bbf2783d04

📥 Commits

Reviewing files that changed from the base of the PR and between f59b950 and a063a62.

📒 Files selected for processing (5)
  • ktor-client/ktor-client-core/api/ktor-client-core.api
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
  • ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt
  • ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpStatementTest.kt
  • ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/client/TestEngineWebsocketSession.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api

Copy link
Copy Markdown
Member

@e5l e5l left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check the ci

suspendCancellableCoroutine { cont ->
outgoing.invokeOnClose { ex ->
if (ex == null) {
cont.resume(Unit)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can throw if cont cancelled

@PublishedApi
@OptIn(InternalAPI::class)
internal suspend fun HttpResponse.cleanup() {
internal suspend fun HttpResponse.cleanup(cause: Throwable? = null) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just commenting here because I opened the ticket on YouTrack and I'm interested in the details of the fix:

Wouldn't it be better to remove the default null value of this cause param, and pass in null explicitly in the call in fetchResponse(), such as not to add a cleanup$default method to the published JVM API?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense
Thank you for the comment!

@zibet27 zibet27 requested review from e5l and gervaisj March 13, 2026 13:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt (1)

229-235: Preserve existing cancellation exceptions instead of always wrapping.

When cause is already a CancellationException, pass it through unchanged to keep cancellation diagnostics cleaner. Kotlin coroutines documentation explicitly recommends not wrapping existing cancellation exceptions, as this preserves the original cancellation context and avoids masking the root cause.

♻️ Proposed tweak
 internal suspend fun HttpResponse.cleanup(cause: Throwable?) {
     val job = coroutineContext.job as CompletableJob

     job.apply {
         if (cause != null) {
-            cancel(CancellationException("Exception occurred during request execution", cause))
+            val cancellation = cause as? CancellationException
+                ?: CancellationException("Exception occurred during request execution", cause)
+            cancel(cancellation)
         } else {
             complete()
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt`
around lines 229 - 235, In HttpResponse.cleanup, when cancelling the job
obtained via coroutineContext.job as CompletableJob, detect if the incoming
cause is already a CancellationException and pass it through unchanged (call
cancel(cause)) instead of always wrapping it in a new CancellationException;
otherwise continue wrapping with CancellationException("Exception occurred
during request execution", cause). This change should be made in the internal
suspend fun HttpResponse.cleanup(cause: Throwable?) where job.apply currently
calls cancel(...).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt`:
- Around line 229-235: In HttpResponse.cleanup, when cancelling the job obtained
via coroutineContext.job as CompletableJob, detect if the incoming cause is
already a CancellationException and pass it through unchanged (call
cancel(cause)) instead of always wrapping it in a new CancellationException;
otherwise continue wrapping with CancellationException("Exception occurred
during request execution", cause). This change should be made in the internal
suspend fun HttpResponse.cleanup(cause: Throwable?) where job.apply currently
calls cancel(...).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ccef1b24-3fc6-4358-b6a1-cde1ef4245f5

📥 Commits

Reviewing files that changed from the base of the PR and between c8f1b5a and 1a0c6be.

📒 Files selected for processing (3)
  • ktor-client/ktor-client-core/api/ktor-client-core.api
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
  • ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • ktor-client/ktor-client-core/api/ktor-client-core.klib.api
  • ktor-client/ktor-client-core/api/ktor-client-core.api

Copy link
Copy Markdown

@gervaisj gervaisj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's good to go, but I have been thinking about I slight extension to the issue, if your workflow permits adding to the scope a bit.

Specifically, what if, instead of throwing an exception, body or execute simply completes? (like in the snippet below)

client.prepareGet("...").body { channel: ByteReadChannel ->
    while (true) {
        // imagine a line is sent by the server only every minute
        val line = channel.readLine()
        if (line == "text I'm interested in")
            return@body
    }
}

Then, wouldn't it be nice to have the same behavior as when throwing an exception, i.e. have the streaming call cancel immediately, without waiting for another frame to arrive?

I feel this would match the already documented behavior of execute:

The response object should not be accessed outside of [block] as it will be canceled upon block completion.

A similar line is also present on body. Note that "block completion" here makes no distinction between exceptional and normal completion.

So I've tried to see if cleanup could always cancel the call in this commit: gervaisj@76a525c (which is on this branch: gervaisj/client-always-cancel-streaming).

The only issue I ran across were the tests in ServerSentEventsTest failing, but this was because HttpClient.processSession (which is used by all SSE functions) did not respect body's contract of not using the response after completion of the block. That should be fixed in the commit referenced above, along with 2 new test cases, which are basically the same as those added by @zibet27, but for normal completion of block instead of throwing an exception.

@zibet27 @e5l what do you think?

@zibet27
Copy link
Copy Markdown
Collaborator Author

zibet27 commented Mar 16, 2026

I feel this would match the already documented behavior of execute:

Agree here, afaiu it's intended behaviour and "works" on some engines. I tried to run your tests on this branch, and it succeeded on OkHttp, Apache and Android, but not on CIO and Java @e5l @bjhham?

So I've tried to see if cleanup could always cancel the call in this commit: gervaisj@76a525c (which is on this branch: gervaisj/client-always-cancel-streaming).

Looks good to me!
I would create a separate pull request for it.

@gervaisj
Copy link
Copy Markdown

I tried to run your tests on this branch, and it succeeded on OkHttp, Apache and Android, but not on CIO and Java

Without cancelling the call unconditionally (like is done in my commit), then yes those tests won't work because we fall in the complete() branch, and so hang on join(), and then time out.

I would create a separate pull request for it.

Sure!

@gervaisj
Copy link
Copy Markdown

I would create a separate pull request for it.

@zibet27 Sorry, maybe there was a misunderstanding, were you waiting for an action on my part? Is there something missing for this PR?

@zibet27
Copy link
Copy Markdown
Collaborator Author

zibet27 commented Mar 23, 2026

Is there something missing for this PR?

@gervaisj No, I'm waiting for approvals

Copy link
Copy Markdown
Member

@osipxd osipxd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I think we should include this fix into minor release not to introduce new API in a patch release.

job.apply {
complete()
if (cause != null) {
cancel(CancellationException("Exception occurred during request execution", cause))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a chance the cause is already CancellationException. Probably we shouldn't wrap it with another CancellationException in this case.
Please add a test checking this behavior if you decide to fix it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpResponse.cleanup is not tested directly anywhere
Should we?

public fun <init> (Lio/ktor/client/request/HttpRequestBuilder;Lio/ktor/client/HttpClient;)V
public final fun cleanup (Lio/ktor/client/statement/HttpResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun cleanup (Lio/ktor/client/statement/HttpResponse;Ljava/lang/Throwable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final synthetic fun cleanup (Lio/ktor/client/statement/HttpResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's switch the base branch to main

@zibet27 zibet27 force-pushed the zibet27/client-cancel-streaming-on-exception branch from 1a0c6be to 4dd9571 Compare March 24, 2026 17:01
@zibet27 zibet27 changed the base branch from release/3.x to main March 24, 2026 17:01
@zibet27 zibet27 requested a review from osipxd March 24, 2026 17:02
@zibet27 zibet27 enabled auto-merge (squash) March 27, 2026 15:05
@zibet27 zibet27 merged commit 2719c3f into main Mar 31, 2026
14 of 17 checks passed
@zibet27 zibet27 deleted the zibet27/client-cancel-streaming-on-exception branch March 31, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants